package com.espirit.eap.util;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.log4j.Logger;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.espirit.eap.pagelayout.AvabButton;
import com.espirit.eap.pagelayout.BlockSection;
import com.espirit.eap.pagelayout.Element;
import com.espirit.eap.pagelayout.FlatSection;
import com.espirit.eap.pagelayout.GridSection;
import com.espirit.eap.pagelayout.Layout;
import com.espirit.eap.pagelayout.LinkTip;
import com.espirit.eap.pagelayout.LinkedButton;
import com.espirit.eap.pagelayout.Model;
import com.espirit.eap.pagelayout.ModelField;
import com.espirit.eap.pagelayout.PageCache;
import com.espirit.eap.pagelayout.PageReference;
import com.espirit.eap.pagelayout.SectionElement;
import com.espirit.eap.pagelayout.TabbedSection;
import com.espirit.eap.pagelayout.TagCategory;
import com.espirit.eap.pagelayout.TagCategoryResource;
import com.espirit.eap.pagelayout.TagInfo;
import com.espirit.eap.pagelayout.TagInfoProperty;
import com.espirit.eap.pagelayout.TaglibImport;
import com.espirit.eap.util.tagAdaptor.TagAdaptorUtil;
import com.espirit.eap.util.tagAdaptor.TagAvabButtonAdaptor;
import com.espirit.eap.util.tagAdaptor.TagBlockSectionAdaptor;
import com.espirit.eap.util.tagAdaptor.TagElementAdaptor;
import com.espirit.eap.util.tagAdaptor.TagGridSectionAdaptor;
import com.espirit.eap.util.tagAdaptor.TagLinkTipAdaptor;
import com.espirit.eap.util.tagAdaptor.TagPageReferenceAdaptor;
import com.espirit.eap.util.tagAdaptor.TagSectionElementAdaptor;
import com.espirit.eap.util.tagAdaptor.TagTabbedSectionAdaptor;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.oosearch.QLParse;
import com.googlecode.cswish.struts.ITaglibManager;
import com.googlecode.cswish.struts.hibernate.HibernateUtil;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.GenericJPAService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FileUtil;
import com.googlecode.cswish.util.InvocationUtil;
import com.googlecode.cswish.util.OgnlExUtil;

import freemarker.core.Comment;
import freemarker.core.FMParser;
import freemarker.core.Macro;
import freemarker.core.TemplateElement;
import freemarker.template.Configuration;
import freemarker.template.Template;

@Service
public class TaglibManager implements ITaglibManager {
	
	private final static Logger logger = Logger.getLogger(TaglibManager.class);
	
	@Resource
	private QLParse qlParse;
	
	@Resource
	private GenericJPAService genericJPAService;
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private Config config;
	
	@Resource
	private TagBlockSectionAdaptor tagBlockSectionAdaptor;
	
	@Resource
	private TagGridSectionAdaptor tagGridSectionAdaptor;
	
	@Resource
	private TagTabbedSectionAdaptor tagTabbedSectionAdaptor;
	
	@Resource
	private TagSectionElementAdaptor tagSectionElementAdaptor;
	
	@Resource
	private TagAvabButtonAdaptor tagAvabButtonAdaptor;
	
	@Resource
	private TagPageReferenceAdaptor tagPageReferenceAdaptor;
	
	@Resource
	private TagLinkTipAdaptor tagLinkTipAdaptor;
	
	@Resource
	private TagAdaptorUtil tagAdaptorUtil;
	
	@Resource
	private OgnlExUtil ognlExUtil;
	
	@Resource
	private InvocationUtil invocationUtil;
	
	@Resource
	private CacheService cacheService;
	
	private TaglibManager taglibManager;
	
	// Full tagName --> tagInfo
	private Map<String, TagInfo> defaultTagProperties;
	
	// category --> Tagcategory, group tag by category
	private Map<String, TagCategory> categories;
	
	/////////////// hardcode the information for tag conversion because of legacy reason //////////////////
	// tagName --> tag class (Element class)
	private Map<String, Class> tagClasses;
	// tagName --> tag class type
	private Map<String, Class> tagPropertyTypes;
	// Element class name --> adaptor, used by the conversion
	private Map<String, TagElementAdaptor> adaptors;

	private Map<String, String> compatibleTags;
	private Collection<String> modelFieldTags;
	
	// for the tag file cache check
	private long checkTime;
	private long modifyTime;
	
	public TaglibManager() {
	}
	
	private TaglibManager getThis() {
		if (taglibManager == null) {
			taglibManager = ObjectFactory.getInstance().getBean(TaglibManager.class, false);
		}
		return taglibManager;
	}
	
	@PostConstruct
	private void init() {
		tagClasses = new HashMap<String, Class>();
		
		tagClasses.put("container", FlatSection.class);
		tagClasses.put("pageinfo", GridSection.class);
		tagClasses.put("datagrid", GridSection.class);
		tagClasses.put("tab", TabbedSection.class);
		tagClasses.put("textarea", ModelField.class);
		tagClasses.put("checkboxlist", ModelField.class);
		tagClasses.put("modelselect", ModelField.class);
		tagClasses.put("combobox", ModelField.class);
		tagClasses.put("enumselect", ModelField.class);
		tagClasses.put("comboBox", ModelField.class);
		tagClasses.put("number", ModelField.class);
		tagClasses.put("date", ModelField.class);
		tagClasses.put("text", ModelField.class);
		tagClasses.put("checkbox", ModelField.class);
		tagClasses.put("button", AvabButton.class);
		tagClasses.put("ebutton", AvabButton.class);
		tagClasses.put("linkedbutton", LinkedButton.class);
		tagClasses.put("include", PageReference.class);
		tagClasses.put("script", PageReference.class);
		tagClasses.put("css", PageReference.class);
		
		adaptors = new HashMap<String, TagElementAdaptor>();
		// common adaptor
		adaptors.put(BlockSection.class.getName(), tagBlockSectionAdaptor);
		adaptors.put(SectionElement.class.getName(), tagSectionElementAdaptor);
		
		// detail adaptor
		adaptors.put(FlatSection.class.getName(), tagBlockSectionAdaptor);
		adaptors.put(GridSection.class.getName(), tagGridSectionAdaptor);
		adaptors.put(TabbedSection.class.getName(), tagTabbedSectionAdaptor);
		adaptors.put(ModelField.class.getName(), tagSectionElementAdaptor);
		adaptors.put(AvabButton.class.getName(), tagAvabButtonAdaptor);
		adaptors.put(PageReference.class.getName(), tagPageReferenceAdaptor);
		adaptors.put(LinkTip.class.getName(), tagLinkTipAdaptor);
		
		tagPropertyTypes = new HashMap<String, Class>();
		// skip container, tab, button, ebutton, linkedbutton, include
		tagPropertyTypes.put("pageinfo", List.class);
		tagPropertyTypes.put("datagrid", List.class);
		tagPropertyTypes.put("textarea", String.class);
		tagPropertyTypes.put("checkboxlist", List.class);
		// Model.class if the property is different with label property
		tagPropertyTypes.put("select", String.class);
		tagPropertyTypes.put("modelselect", String.class);
		tagPropertyTypes.put("combobox", String.class);
		tagPropertyTypes.put("enumselect", String.class);
		tagPropertyTypes.put("comboBox", String.class);
		tagPropertyTypes.put("number", BigDecimal.class);
		tagPropertyTypes.put("price", BigDecimal.class);
		tagPropertyTypes.put("date", Date.class);
		tagPropertyTypes.put("text", String.class);
		tagPropertyTypes.put("checkbox", Boolean.class);
		
		compatibleTags = new LinkedHashMap<String, String>();
		compatibleTags.put("text", "");
		compatibleTags.put("textarea", "");
		compatibleTags.put("date", "");
		compatibleTags.put("number", "");
		compatibleTags.put("price", "");
		compatibleTags.put("password", "");
		compatibleTags.put("checkbox", "");
		compatibleTags.put("checkboxlist", "");
		compatibleTags.put("radio", "");
		compatibleTags.put("file", "");
		
		compatibleTags.put("enumselect", "");
		compatibleTags.put("select", "");
		compatibleTags.put("modelselect", "");
		compatibleTags.put("dictselect", "");
		compatibleTags.put("combobox", "");
		compatibleTags.put("i18n", "");
		compatibleTags.put("hidden", "");
		compatibleTags.put("treebox", "");
		
		
		defaultTagProperties = cacheService.newLinkedCache(TagInfo.class, null);
		categories = cacheService.newLinkedCache(TagCategory.class, null);
	}
	
	public Layout createLayout() {
		Layout layout = new Layout();
		//layout.setCustomVersion(true);
		return layout;
	}
	
	public Model createModel(String modelName) {
		Model model = new Model();
		String simpleName = beanIntrospector.getSimpleName(modelName);
		model.setName(simpleName);
		return model;
	}

	/**
	 * Get the Element class according to the tag, such as textarea --> ModelField.class 
	 * 
	 * @param tagName
	 * @return
	 */
	public Class getTagElementClass(String tagName) {
		Class clazz = tagClasses.get(tagName);
		if (clazz == null) {
			TagInfo tagInfo = getDefaultTagProperties().get(tagName);	// the common tag can use the short name to get the tagInfo
			if (tagInfo != null && tagInfo.getContainer() != null && tagInfo.getContainer()) {
				clazz = BlockSection.class;
			} else {
				// default value
				clazz = ModelField.class;
			}
		}
		return clazz;
	}
	
	/**
	 * The java property type of the tag, such as textarea --> String.class
	 * 
	 * @param tagName
	 * @return
	 */
	public Class guessTagPropertyType(String tagName) {
		return tagPropertyTypes.get(tagName);
	}
	
	/**
	 * The java property type of the element, such as ModelField (DefModel=java.util.List) --> List.class
	 * 
	 * @param tagType
	 * @return
	 */
	public Class guessElementPropertyType(Element element) {
		Class propertyType = null;
		if (element != null) {		
			String tagName;
			TagElementAdaptor adaptor = adaptors.get(element.getClass().getName());
			if (adaptor != null) {
				tagName = adaptor.getTagName(element);
				
				propertyType = guessTagPropertyType(tagName);
			}
		}
		return propertyType;
	}
	
	public Collection<TagInfo> getTagInfos() {
		return getDefaultTagProperties().values();
	}
	
	private Map<String, TagInfo> getDefaultTagProperties() {
		if (defaultTagProperties.isEmpty()) {
			getThis().initTagProperty();
		}
		return defaultTagProperties;
	}
	
	public Collection<TagInfo> getTagInfos(boolean isContainer, boolean containPrivate) {
		List<TagInfo> list = new ArrayList<TagInfo>();
		for (TagInfo tagInfo : getTagInfos()) {
			if (containPrivate || !(tagInfo.getPrivateScope() != null && tagInfo.getPrivateScope())) {
				if (tagInfo.getContainer() != null && tagInfo.getContainer() == isContainer) {
					list.add(tagInfo);
				}
			}
		}
		return list;
	}
	
	//////////////////////// default tag info /////////////////////////////////
	public TagInfo getDefaultTagInfo(List<String> categories, String tagName) {
		if (categories == null || categories.isEmpty()) {
			return getDefaultTagInfo((String)null, tagName);
		} else {
			for (String category : categories) {
				TagInfo tagInfo = getDefaultTagProperties().get(category + '.' + tagName);
				if (tagInfo != null) {
					return tagInfo;
				}
			}
			
			// no found, try to find from the default tag category
			return getDefaultTagInfo((String)null, tagName);
		}
	}
	
	public TagInfo getDefaultTagInfo(String category, String tagName) {
		return getDefaultTagProperties().get(category == null ? tagName : category + '.' + tagName);
	}
	
	// get the tagInfo from a group of tag category
	public Map<String, Object> getDefaultTagProperty(List<String> categories, String tagName) {
		TagInfo tagInfo = getDefaultTagInfo(categories, tagName);
		if (tagInfo != null) {
			return tagInfo.getDefaultProperties();
		} else {
			return Collections.EMPTY_MAP;
		}
	}
	
	public Map<String, Object> getDefaultTagProperty(String category, String tagName) {
		TagInfo tagInfo = getDefaultTagInfo(category, tagName);
		if (tagInfo != null) {
			return tagInfo.getDefaultProperties();
		} else {
			return Collections.EMPTY_MAP;
		}
	}
	///////////////////////////////////////////////////////////////////////////
	
	public TagCategory getTagCategory(String category) {
		return categories.get(category);
	}
	
	public Collection<String> getCategories() {
		getDefaultTagProperties();
		return categories.keySet();
	}
	
	public TagCategory findTagCategoryByPath(String path) {
		if (path == null) {
			return null;
		}
		
		for (TagCategory tagCategory : categories.values()) {
			if (path.equals(tagCategory.getPath())) {
				return tagCategory;
			}
		}
		return null;
	}
	
	// FIXME: don't use the hard code logic
	public Collection<String> getIncludeCssFiles(Collection<String> tagNames) {
		Map<String, TagInfo> infos = getDefaultTagProperties();
		Set<String> categories = new HashSet<>();
		for (String tagName : tagNames) {
			TagInfo tagInfo = infos.get(tagName);
			if (tagInfo != null) {
				categories.add(tagInfo.getTagCategory().getName());
			}
		}
		
		List<String> cssFiles = new ArrayList<String>();
		for (String category : categories) {
			if ("common".equals(category)) {
				cssFiles.add("/static/espirit/styles/jtaglib.css");
			} else if ("bscommon".equals(category)) {
				cssFiles.add("/static/espirit/scripts/bootstrap/css/bootstrap.css");
			}
		}
		return cssFiles;
	}
	
	public List<TagInfo> getTagInfoByCategory(String category, boolean refresh) {
		TagCategory tagCategory = categories.get(category);
		if (tagCategory == null) {
			return null;
		}
		
		if (refresh) {
			if (tagCategory.isModified(true)) {
				try {
					for (TagInfo tagInfo : tagCategory.getTagInfos()) {
						defaultTagProperties.remove(tagInfo.getTagCategory().getName() + '.' + tagInfo.getTagName());
					}
					// remove the short key
					if (isCommonCategory(category)) {
						for (TagInfo tagInfo : tagCategory.getTagInfos()) {
							defaultTagProperties.remove(tagInfo.getTagCategory().getName());
						}
					}
					categories.remove(category);
					
					List<org.springframework.core.io.Resource> resources = tagCategory.getResources();
					tryToUpdateTagFileToDb(tagCategory.getName(), resources);
					
					tagCategory = categories.get(category);
				} catch (Exception ex) {
					logger.error("Failed to init tag category:" + tagCategory.getName(), ex);
				}
			}
		}
		return tagCategory.getTagInfos();
	}
	
	public Collection<String> getStdCategories() {
		// TODO: support the customize setting, each site has itself standard category
		List<String> stdCategories = new ArrayList<String>(3);
		stdCategories.add("core");
		stdCategories.add("common");
		stdCategories.add("container");
		stdCategories.add("graph");
		return stdCategories;
	}
	
	public Collection<String> getNonStdCategories() {
		Collection<String> categories = getCategories();
		Collection<String> stdCategories = getStdCategories();
		return CollectionUtils.subtract(categories, stdCategories);
	}
	
	public Collection<TagInfo> getTagInfos(String category, boolean containPrivate) {
		List<TagInfo> list = new ArrayList<TagInfo>();
		for (TagInfo tagInfo : getTagInfos()) {
			if (containPrivate || !(tagInfo.getPrivateScope() != null && tagInfo.getPrivateScope())) {
				if (tagInfo.getTagCategory().getName().equals(category)) {
					list.add(tagInfo);
				}
			}
		}
		return list;
	}
	
	public List<Element> findElementsByType(List<Element> elements, String tagName) {
		List<Element> results = new ArrayList<Element>();
		for (Element element : elements) {
			scanElementByType(element, tagName, results);
		}
		return results;
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void scanElementByType(Element element, String tagName, List<Element> results) {
		TagElementAdaptor adaptor = getElementAdaptor(element);
		if (adaptor != null) {
			if (tagName.equals(adaptor.getTagName(element))) {
				results.add(element);
			}
		}
		
		List<Element> children = element.getChildren();
		if (children != null && children.size() > 0) {
			for (Element child : children) {
				scanElementByType(child, tagName, results);
			}
		}
	}
	
	/**
	 * Find the specified element by name or type
	 * 
	 * @param elements
	 * @param name
	 * @param isCtn
	 * @return
	 */
	public Element findElement(List<Element> elements, String name) {
		if (elements == null) {
			return null;
		}
		
		Element ret = null;
		int index = name.lastIndexOf("_gen_");
		if (index >= 0) {
			String suffix = name.substring(name.lastIndexOf('_') + 1);
			Integer id;
			if (NumberUtils.isNumber(suffix)) {
				id = Integer.valueOf(suffix);
			} else {
				id = null;
			}
			for (Element element : elements) {
				ret = findElement(element, null, name, id);
				if (ret != null) {
					break;
				}
			}
		} else {
			for (Element element : elements) {
				ret = findElement(element, null, name, null);
				if (ret != null) {
					break;
				}
			}
		}
		return ret;
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private <T> T findElement(Element element, String parentNamePrefix, String matchedName, Integer matchedId) {
		// compare the element
		if (matchedId != null && matchedId.equals(element.getId())
				|| matchedName != null && matchedName.equals(tagAdaptorUtil.getFullName(parentNamePrefix, element))) {		// matched name
			element = (Element) HibernateUtil.unproxy(element);
			if (getElementAdaptor(element) != null) {
				return (T) element;
			}
		}
		
		T found = null;
		List<Element> children = element.getChildren();
		String namePrefix = element.getNamePrefix();
		if (namePrefix == null) {
			namePrefix = parentNamePrefix;
		} else {
			if (parentNamePrefix != null) {
				if (namePrefix.equals("model") || namePrefix.equals("execute")) {
					// ignore it, use the parent directly
					namePrefix = parentNamePrefix;
				} else {
					namePrefix = parentNamePrefix + '.' + namePrefix;
				}
			}
		}
		
		// continue to find child only when:
		// 1. don't find element by name
		// or 2. matched name starts with the name prefix
		// or 3. name prefix is invalid (which means the name prefix is '')
		if (StringUtils.isEmpty(namePrefix) || matchedName == null || matchedName.startsWith(namePrefix) || matchedName.startsWith("_gen_")) {
			if (namePrefix != null && namePrefix.startsWith("_gen_")) {
				namePrefix = parentNamePrefix;
			}
			if (children != null && children.size() > 0) {
				for (Element child : children) {
					found = findElement(child, namePrefix, matchedName, matchedId);
					if (found != null) {
						break;
					}
				}
			}
		}
		return found;
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public TagInfo getTagInfo(Layout root, Element element, Map<String, Object> context, Map<Element, TagInfo> cache) {
		if (cache != null) {
			TagInfo tagInfo = cache.get(element);
			if (tagInfo != null) {
				return tagInfo;
			}
		}
		
		element = (Element) HibernateUtil.unproxy(element);
		TagElementAdaptor elementAdaptor = getElementAdaptor(element);
		if (elementAdaptor == null) {
			return null;
		}
		TagInfo tagInfo = elementAdaptor.getTagProperties(root, element, context);
		if (tagInfo == null) {
			return null;
		}
		Map<String, Object> propertiesMap = tagInfo.getTagProperties();

		// combine the tag property by order
		Map<String, Object> defaultTagProperty = getDefaultTagProperty(root.getCategories(), tagInfo.getTagName());
		Map<String, Object> combineTagProperty = new LinkedHashMap<String, Object>(defaultTagProperty.size() * 4 / 3);
		for (Entry<String, Object> keyValue : defaultTagProperty.entrySet()) {
			Object dbValue = propertiesMap.get(keyValue.getKey());
			if (dbValue == null || "".equals(dbValue) || "[]".equals(dbValue)) {
				combineTagProperty.put(keyValue.getKey(), keyValue.getValue());		// use the default setting
			} else {
				// Rule: the tag must has the default value if it's not string type
				// convert the db property to the matched type (parts of db properties come from exProperty)
				if (keyValue.getValue() != null) {
					Class targetClass = keyValue.getValue().getClass();
					if (!targetClass.isAssignableFrom(dbValue.getClass())) {
						boolean converted = false;
						if (dbValue instanceof String) {
							String sDBValue = (String) dbValue;
							if (sDBValue.length() > 2 && sDBValue.charAt(0) == '[' 
									&& sDBValue.charAt(sDBValue.length() - 1) == ']' ) {
								// TODO: use JSONUtil.parse?
								if (sDBValue.indexOf('{') == -1) {
									String [] values = StringUtils.split(sDBValue.substring(1, sDBValue.length() - 1), ',');
									converted = true;
									List<String> convertValue = new ArrayList<String>(values.length);
									for (String value : values) {
										String trim = value.trim();
										if (trim.length() == 0) {
											convertValue.add(trim);
										} else {
											if (trim.charAt(0) == '\'' && trim.charAt(trim.length() - 1) == '\''
													|| trim.charAt(0) == '"' && trim.charAt(trim.length() - 1) == '"') {
												convertValue.add(trim.substring(1, trim.length() - 1));
											} else {
												convertValue.add(trim);
											}
										}
										
									}
									dbValue = convertValue;
								} else {
									// see PageConverterService.appendProperty
									converted = true;
									List<String> list = new ArrayList<String>();
									int lastIndex = 1;	// skip first ']'
									int enter = 0;
									for (int i = 0, len = sDBValue.length(); i < len; i++) {
										char c = sDBValue.charAt(i);
										if (c == '{') {
											enter++;
										} else if (c == '}') {
											enter--;
										} else if (c == ',' && enter == 0) {
											list.add(sDBValue.substring(lastIndex, i));
											lastIndex = i + 1;
										}
									}
									if (lastIndex < sDBValue.length() - 1) {	// skipt last ']'
										list.add(sDBValue.substring(lastIndex, sDBValue.length() - 1));
									}
									dbValue = list;
								}
							} else if (Collection.class.isAssignableFrom(targetClass)) {
								// keep the original string value
								converted = true;
							}
						}
						if (!converted) {
							Object convertedDbValue = ognlExUtil.convertValue(dbValue, targetClass, keyValue.getKey());
							if (convertedDbValue != null) {
								dbValue = convertedDbValue;
							}
						}
					}
				}
				combineTagProperty.put(keyValue.getKey(), dbValue);
			}
		}
		for (Entry<String, Object> keyValue : propertiesMap.entrySet()) {
			if (!combineTagProperty.containsKey(keyValue.getKey())) {
				combineTagProperty.put(keyValue.getKey(), keyValue.getValue());
			}
		}
		
		TagInfo defaultInfo = getDefaultTagInfo(root.getCategories(), tagInfo.getTagName());
		if (defaultInfo != null) {
			tagInfo.setTagCategory(defaultInfo.getTagCategory());
			tagInfo.setContainer(defaultInfo.getContainer());
			tagInfo.setId(defaultInfo.getId());
			tagInfo.setModelField(defaultInfo.getModelField());
			tagInfo.setPrivateScope(defaultInfo.getPrivateScope());
		}
		
		tagInfo.setTagProperties(combineTagProperty);
		tagInfo.setDefaultProperties(defaultTagProperty);
		if (cache != null) {
			cache.put(element, tagInfo);
		}
		return tagInfo;
	}
	
	private TagElementAdaptor getElementAdaptor(Element element) {
		if (element != null) {
			return getElementAdaptor(element.getClass());
		} else {
			return null;
		}
	}
	
	private TagElementAdaptor getElementAdaptor(Class clazz) {
		TagElementAdaptor elementAdaptor = adaptors.get(clazz.getName());
		clazz = clazz.getSuperclass();
		while (elementAdaptor == null && clazz != null) {
			elementAdaptor = adaptors.get(clazz.getName());
			clazz = clazz.getSuperclass();
		}
		return elementAdaptor;
	}
	
	public String getTagFolder() {
		return config.getViewDir() + "taglib/";
	}
	
	// step 1. db tag information
	// step 2. file tag information
	@Transactional
	public synchronized void initTagProperty() {
		if (!defaultTagProperties.isEmpty()) {
			return;
		}
		
		//boolean syncTagDb = false;		// false --> Use the special job or method to do this thing
		// FIXME: how to rebuild the db record? delete the table data?
		initDbTagCategory();
		
		String webRootPath = config.getRealWebRootPath();
		String sTagFolder = getTagFolder();
		File tagFolder = new File(webRootPath + sTagFolder);
		boolean hasTagFile = tagFolder.exists() && tagFolder.list().length > 0;
		Map<String, List<org.springframework.core.io.Resource>> groupResources = new LinkedHashMap<>();
		// the default order
		groupResources.put("container", new ArrayList<org.springframework.core.io.Resource>());
		groupResources.put("common", new ArrayList<org.springframework.core.io.Resource>());
		if (hasTagFile) {
			for (File taglibFile : tagFolder.listFiles(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {
					// 1. only scan ftl file
					// 2. skip the control tag, such as if_control, when_control etc
					return name.endsWith(".ftl") && !name.endsWith("control.ftl");
				}
			})) {
				org.springframework.core.io.Resource resource = new FileSystemResource(taglibFile);
				String fileName = resource.getFilename();
				int index = fileName.indexOf('.');
				String category = index > 0 ? fileName.substring(0, index) : fileName;
				List<org.springframework.core.io.Resource> resourceList = groupResources.get(category);
				if (resourceList == null) {
					resourceList = new ArrayList<>();
					groupResources.put(category, resourceList);
				}
				resourceList.add(resource);
			}
		}
		
		// try to find the files from class directory
		ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = "classpath*:" + sTagFolder + "*.ftl";
        
        try {
            org.springframework.core.io.Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
            for (org.springframework.core.io.Resource resource : resources) {
            	String fileName = resource.getFilename();
            	if (fileName.endsWith(".ftl") && !fileName.endsWith("control.ftl")) {
            		int index = fileName.indexOf('.');
            		String category = index > 0 ? fileName.substring(0, index) : fileName;
    				List<org.springframework.core.io.Resource> resourceList = groupResources.get(category);
    				if (resourceList == null) {
    					resourceList = new ArrayList<>();
    					groupResources.put(category, resourceList);
    				}
    				resourceList.add(resource);
            	}
            }
        } catch (Exception ex) {
			logger.error("Failed to load resource:" + packageSearchPath, ex);
		}
		
        String category = null;
		try {
			// initialize the category
	        for (Entry<String, List<org.springframework.core.io.Resource>> entry : groupResources.entrySet()) {
	        	category = entry.getKey();
	        	tryToUpdateTagFileToDb(entry.getKey(), entry.getValue());
	        }
		} catch (Exception ex) {
			logger.error("Failed to read tag file:" + category, ex);
		}
	}
	
	private void initDbTagCategory() {
		List<TagCategory> categoryList = genericJPAService.searchAll(new TagCategory()).getData();
		for (TagCategory tagCategory : categoryList) {
			boolean tagFileExists = tagCategory.getPath() != null && (
					this.getClass().getResource(tagCategory.getPath()) != null
					|| new File(config.getRealWebRootPath() + tagCategory.getPath()).exists());
			if (tagFileExists) {
				updateDbTagTransientField(tagCategory);
			} else {
				genericJPAService.delete(tagCategory);
			}
		}
	}

	// the transient field is 'TagCategory.dependence, css, js' etc 
	private void updateDbTagTransientField(TagCategory tagCategory) {
		String category = tagCategory.getName();
		categories.put(category, (TagCategory)HibernateUtil.unproxy(tagCategory));
		
		for (TagInfo tagInfo : tagCategory.getTagInfos()) {
			String fullName = category + '.' + tagInfo.getTagName();
			defaultTagProperties.put(fullName, tagInfo);
			
			// update the default properties of tagInfo
			Map<String, Object> defaultProperties = new LinkedHashMap<String, Object>();
			for (TagInfoProperty tagInfoProperty : tagInfo.getTagInfoProperties()) {
				Object value = null;
				Class type = ClassUtil.forName(tagInfoProperty.getType());
				if (tagInfoProperty.getValue() != null) {
					value = ognlExUtil.convertValue(tagInfoProperty.getValue(), type);
				}
				if (value == null) {
					value = getInitValue(type);
				}
				defaultProperties.put(tagInfoProperty.getName(), value);
			}
			tagInfo.setDefaultProperties(defaultProperties);
		}
		
		if (isCommonCategory(category)) {
			for (TagInfo tagInfo : tagCategory.getTagInfos()) {
				defaultTagProperties.put(tagInfo.getTagName(), tagInfo);
			}
		}
		
		// update tagCategory cache
		List<String> css = new ArrayList<>();
		List<String> js = new ArrayList<>();
		List<String> dependence = new ArrayList<>();
		List<org.springframework.core.io.Resource> resources = new ArrayList<>();
		for (TagCategoryResource tagCategoryResource : tagCategory.getTagCategoryResources()) {
			switch (tagCategoryResource.getType()) {
			case TagCategoryResource.RESOURCE_TYPE_CSS:
				css.add(tagCategoryResource.getResource());
				break;
			case TagCategoryResource.RESOURCE_TYPE_JS:
				js.add(tagCategoryResource.getResource());
				break;
			case TagCategoryResource.RESOURCE_TYPE_DEPENDENCE:
				dependence.add(tagCategoryResource.getResource());
				break;
			case TagCategoryResource.RESOURCE_TYPE_TEMPLATE:
				String pathPrefix = config.getRealWebRootPath() + getTagFolder();
				File taglibFile = new File(pathPrefix + tagCategoryResource.getResource());
				if (taglibFile.exists()) {
					resources.add(new FileSystemResource(taglibFile));
				}
				break;
			}
		}
		tagCategory.setCss(css);
		tagCategory.setJs(js);
		tagCategory.setDependence(dependence);
		tagCategory.setResources(resources);
	}

	// see parseTagInfo
	private Object getInitValue(Class type) {
		if (String.class.equals(type)) {
			return "";
		} else if (List.class.equals(type)) {
			return new ArrayList<>();
		} else if (Map.class.equals(type)) {
			return new HashMap<>();
		} else if (Number.class.isAssignableFrom(type)) {
			return 0;
		} else if (Boolean.class.equals(type)) {
			return Boolean.TRUE;
		} else {
			return "";
		}
	}

	protected void tryToUpdateTagFileToDb(String category, List<org.springframework.core.io.Resource> resources) throws Exception {
		TagCategory tagCategory = categories.get(category);
		if (tagCategory != null) {
			boolean fileHasNewVersion = false;
			for (org.springframework.core.io.Resource resource : resources) {
				fileHasNewVersion = tagCategory.getModifiedOn() == null
						|| resource.lastModified() > tagCategory.getModifiedOn().getTime();
				if (fileHasNewVersion) {
					break;
				}
			}
			
			// no new file version --> the DB version is the newest version
			if (!fileHasNewVersion) {
				return ;
			}
		}
		
		List<TagInfo> tagInfos;
		if (tagCategory == null) {
			tagCategory = new TagCategory(category);
			tagCategory.setResources(resources);
			categories.put(category, tagCategory);
			
			tagInfos = new ArrayList<>();
			tagCategory.setTagInfos(tagInfos);
		} else {
			if (tagCategory.getId() != null) {
				if (tagCategory.getTagInfos() != null) {
					// reset defaultTagProperties
					for (TagInfo tagInfo : tagCategory.getTagInfos()) {
						String key = tagCategory.getName() + '.' + tagInfo.getTagName();
						defaultTagProperties.remove(key);
					}
				}
				
				// reset the db
				if (tagCategory.getTagInfos() != null) {
					tagCategory.getTagInfos().clear();
				}
				if (tagCategory.getTagCategoryResources() != null) {
					tagCategory.getTagCategoryResources().clear();
				}
				tagCategory = genericJPAService.merge(tagCategory);
				tagInfos = tagCategory.getTagInfos();		// reference the existed list
			} else {
				tagInfos = new ArrayList<>();
				tagCategory.setTagInfos(tagInfos);
			}
		}
		
		
		List<String> dependences = new ArrayList<>();
		for (org.springframework.core.io.Resource resource : resources) {
			parseTagInfo(resource, tagCategory, tagInfos, dependences);
		}
		tagCategory.setDependence(dependences);
		
		String fullPath = null;
		for (org.springframework.core.io.Resource resource : resources) {
			String filename = resource.getFilename();
			if (filename.substring(0, filename.length() - 4).equals(category)) {		// category.ftl
				fullPath = resource.getURL().getPath();
				break;
			}
		}
		if (fullPath == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Can't find the standard tag file: " + category + ".ftl, use the first resource as the tag file");
			}
			fullPath = resources.get(0).getURL().getPath();
		}
		int index = fullPath.lastIndexOf(getTagFolder());
		String mainPath = fullPath.substring(index);
		tagCategory.setPath(mainPath);
		tagCategory.setModifiedOn(Calendar.getInstance().getTime());

		addTagCategoryResource(tagCategory, tagCategory.getCss(), TagCategoryResource.RESOURCE_TYPE_CSS);
		addTagCategoryResource(tagCategory, tagCategory.getJs(), TagCategoryResource.RESOURCE_TYPE_JS);
		addTagCategoryResource(tagCategory, tagCategory.getDependenceCategories(), TagCategoryResource.RESOURCE_TYPE_DEPENDENCE);
		addTagCategoryResource(tagCategory, tagCategory.getResources());
		
		TagCategory  dbTagCategory = genericJPAService.merge(tagCategory);
		updateDbTagTransientField(dbTagCategory);
	}
	
	private void addTagCategoryResource(TagCategory tagCategory, List<String> resources, Integer type) {
		if (resources != null) {
			for (String resource : resources) {
				TagCategoryResource categoryResource = new TagCategoryResource();
				categoryResource.setResource(resource);
				categoryResource.setType(type);
				categoryResource.setTagCategory(tagCategory);
				tagCategory.getTagCategoryResources().add(categoryResource);
			}
		}
	}
	
	private void addTagCategoryResource(TagCategory tagCategory, List<org.springframework.core.io.Resource> resources) {
		if (resources != null) {
			for (org.springframework.core.io.Resource resource : resources) {
				TagCategoryResource categoryResource = new TagCategoryResource();
				try {
					String fullPath = resource.getURL().getPath();
					int index = fullPath.lastIndexOf(getTagFolder());
					categoryResource.setResource(fullPath.substring(index));
				} catch (IOException ex) {
					logger.error("Get file path", ex);
				}
				categoryResource.setType(TagCategoryResource.RESOURCE_TYPE_TEMPLATE);
				categoryResource.setTagCategory(tagCategory);
				tagCategory.getTagCategoryResources().add(categoryResource);
			}
		}
	}
	
	private void parseTagInfo(org.springframework.core.io.Resource resource, TagCategory tagCategory,
			List<TagInfo> tagInfoList, List<String> dependences) throws Exception {
		String fileName = resource.getFilename();
		String content;
		InputStream input = null;
		try {
			input = resource.getInputStream();
			content = IOUtils.toString(input, "utf-8");
		} finally {
			IOUtils.closeQuietly(input);
		}
		// use FMParser to parser the template
		Template template = new Template(fileName, new StringReader(""), new Configuration());
		FMParser parser = new FMParser(template, new StringReader(content), true, true);
		TemplateElement templateElement = parser.Root();
		
		// add macro definition to tagCategory
		String category = tagCategory.getName();
		boolean firstOne = true;
		Enumeration enumeration = templateElement.children();
		Field field = Macro.class.getDeclaredField("args");
		field.setAccessible(true);
		Object preElement = null;
		while (enumeration.hasMoreElements()) {
			Object element = enumeration.nextElement();
			if (firstOne) {
				firstOne = false;
				
				if (element instanceof Comment) {
					String headerComment = ((Comment)element).getText();
					
					int dependIndex = headerComment.indexOf("@Dependence ");
					if (dependIndex > 0) {
						dependIndex += "@Dependence ".length();
						int dependEnd = headerComment.indexOf('\n', dependIndex);
						if (dependEnd == -1) {
							dependEnd = headerComment.length();
						}
						String dependence = headerComment.substring(dependIndex, dependEnd).trim();
						for (String item : dependence.split(" ")) {
							dependences.add(item);
						}
					}
				}
			}
			
			if (element instanceof Macro) {
				Macro macro = (Macro)element;
				if (macro.isFunction()) {
					continue;
				}
				
				String[] argumentNames = macro.getArgumentNames();
				Map<String, Object> arguments = (Map<String, Object>)field.get(macro);
				
				boolean isPrivate = false;
				boolean isModelField = false;
				if (preElement instanceof Comment) {
					Comment macroComment = (Comment)preElement;
					String comment = macroComment.getText();
					isPrivate = comment.indexOf("@Private") >= 0;
					isModelField = comment.indexOf("@ModelField") >= 0;
				}

				String tagName = macro.getName();
				String tagKey = category + '.' + tagName;
				if (defaultTagProperties.containsKey(tagKey)) {		// avoid to add the duplicated tag definition
					continue;
				}
				
				Map<String, Object> properties = new LinkedHashMap<String, Object>();
				List<TagInfoProperty> tagInfoProperties = new ArrayList<>();
				TagInfo tagInfo = new TagInfo(isPrivate, tagName, tagCategory, properties, null);
				tagInfo.setTagInfoProperties(tagInfoProperties);
				
				// build property
				for (String key : argumentNames) {
					Object item = arguments.get(key);
					String type;
					if (item == null) {
						properties.put(key, null);
						type = String.class.getName();
					} else {
						String className = item.getClass().getName();
						String value = item.toString();
						if (className.equals("freemarker.core.StringLiteral")) {
							// remove the first '"' and the last '"'
							properties.put(key, value.substring(1, value.length() - 1));
							type = String.class.getName();
						} else if (className.equals("freemarker.core.NumberLiteral")
								|| className.equals("freemarker.core.UnaryPlusMinusExpression")) {
							Number numberValue = NumberUtils.createNumber(value);
							properties.put(key, numberValue);
							type = numberValue == null ? Integer.class.getName() : numberValue.getClass().getName();
						} else if (className.equals("freemarker.core.BooleanLiteral")) {
							if ("true".equals(value)) {
								properties.put(key, Boolean.TRUE);
							} else {
								properties.put(key, Boolean.FALSE);
							}
							type = Boolean.class.getName();
						} else if (className.equals("freemarker.core.ListLiteral")) {
							// TODO: use the real collection?
							properties.put(key, Collections.EMPTY_LIST);
							type = List.class.getName();
						} else if ("{}".equals(value)) {
							properties.put(key, Collections.EMPTY_MAP);
							type = Map.class.getName();
						} else {
							logger.error("unkonwn type, ignore the default value: " + value);
							properties.put(key, null);
							type = String.class.getName();
						}
					}
					
					// build TagInfoProperty
					TagInfoProperty tagInfoProperty = new TagInfoProperty();
					tagInfoProperty.setTagInfo(tagInfo);
					tagInfoProperty.setName(key);
					Object value = properties.get(key);
					if (value != null) {
						String str = ognlExUtil.convertValue(value, String.class);
						tagInfoProperty.setValue(str);
					}
					tagInfoProperty.setType(type);
					tagInfoProperties.add(tagInfoProperty);
				}
				
				boolean hasNested = macro.getChildNodes().toString().indexOf("<#nested") > 0;
				tagInfo.setContainer(hasNested);
				tagInfo.setModelField(isModelField);				
				tagInfoList.add(tagInfo);
			}
			
			preElement = element;
		}
	}
	
	private boolean isCommonCategory(String category) {
		return "core".equals(category) || "common".equals(category) || "container".equals(category);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void updateToElement(Element element, Map<String, String> prop, Map<String, Object> context, PageCache pageCache) {
		TagElementAdaptor elementAdaptor = getElementAdaptor(element);
		
		elementAdaptor.updateToElement(prop, element, context, pageCache);
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public Element addElement(Element parent, 
			String category, String tagName, Map<String, Object> tagProperties, PageCache pageCache) {
		TagElementAdaptor elementAdaptor = getElementAdaptor(getTagElementClass(tagName));
		
		if (category == null) {
			// use the default category
			TagInfo tagInfo = getDefaultTagInfo((String)null, tagName);
			if (tagInfo != null) {
				category = tagInfo.getTagCategory().getName();
			}
		}
		
		// process the import category
		Layout layout = pageCache.getLayout();
		if (!layout.containCategory(category)) {
			// condition 1: the tagName contains the category name
			boolean useImport = tagName.indexOf('.') != -1;
			if (!useImport) {
				// condition 2: the exists category contains the tag name
				List<String> categories = layout.getCategories();
				if (categories != null) {
					for (String existedCategory : categories) {
						useImport = getDefaultTagInfo(existedCategory, tagName) != null;
						if (useImport) {
							break;
						}
					}
				}
			}
			if (useImport) {
				if (!tagName.startsWith(category + '.')) {
					tagName = category + '.' + tagName;
				}
			}
			
			List<String> dependCategoryList = new ArrayList<String>();
			calculateDependCategoryList(dependCategoryList, category);
			for (String dependCategory : dependCategoryList) {
				if (!layout.containCategory(dependCategory)) {
					TagCategory tagCategory = getTagCategory(dependCategory);
					TaglibImport tagImport = new TaglibImport();
					tagImport.setCategory(dependCategory);
					tagImport.setLayout(layout);
					tagImport.setTagCategory(tagCategory);
					tagImport.setUseImport(useImport);
					
					layout.safeTaglibImports().add(tagImport);
				}
			}
		}
		
		return elementAdaptor.addElement(parent, tagName, tagProperties, pageCache);
	}
	
	
	private void calculateDependCategoryList(List<String> dependCategoryList, String category) {
		TagCategory tagCategory = getTagCategory(category);
		if (tagCategory != null && tagCategory.getDependenceCategories() != null) {
			for (String depend : tagCategory.getDependenceCategories()) {
				calculateDependCategoryList(dependCategoryList, depend);
			}
		}
		dependCategoryList.add(category);
	}
	
	
	public void deleteElement(Element parent, Element existed, PageCache pageCache) {
		TagElementAdaptor elementAdaptor = getElementAdaptor(existed);
		
		elementAdaptor.deleteElement(parent, existed, pageCache);
	}
	
	public List<String> getCompatibleTags(String tagName) {
		// TODO: use getModelFieldTags ?
		
		List<String> tags = new ArrayList<String>();
		
		if (compatibleTags.containsKey(tagName)) {
			tags.addAll(compatibleTags.keySet());
		} else {
			tags.add(tagName);			
		}
		
		return tags;
	}
	
	public Collection<String> getModelFieldTags() {
		if (modelFieldTags == null) {
			synchronized (this) {
				if (modelFieldTags == null) {
					modelFieldTags = new HashSet<String>();
					
					for (TagInfo tagInfo : getDefaultTagProperties().values()) {
						if (tagInfo.getModelField() != null && tagInfo.getModelField()) {
							modelFieldTags.add(tagInfo.getTagName());
						}
					}
				}
			}
		}
		
		return modelFieldTags;
	}
	
	public boolean isModelFieldTag(String tagName) {
		return getModelFieldTags().contains(tagName);
	}
	
	public List<String> getTagNamesWithDatasource() {
		List<String> tagNames = new ArrayList<String>();
		for (TagInfo tagInfo : getDefaultTagProperties().values()) {
			boolean hasCollection = tagInfo.getDefaultProperties().containsKey("collection");
			if (hasCollection) {
				tagNames.add(tagInfo.getTagName());
			}
		}
		return tagNames;
	}
	
	// TODO: better solution?
	public long lastModifyTime() {
		if (defaultTagProperties.isEmpty()) {
			return 0;
		}
		
		// check it after 10 seconds gap at least
		if (System.currentTimeMillis() - this.checkTime > FileUtil.fileCheckFrequence) {
			this.checkTime = System.currentTimeMillis();
			
			// get the max modify time
			getDefaultTagProperties();
			Collection<TagCategory> values = categories.values();
			for (TagCategory tagCategory : values) {
				if (tagCategory.getResources() != null) {
					for (org.springframework.core.io.Resource resource : tagCategory.getResources()) {
						try {
							// TODO: use FileUtil.isChanged?
							long categoryModifyTime = resource.lastModified();
							if (categoryModifyTime > modifyTime) {
								modifyTime = categoryModifyTime;
							}
						} catch (IOException ex) {
							if (logger.isDebugEnabled()) {
								logger.debug(resource + " hasn't the modified time");
							}
						}
					}
				}
			}
		}
		return modifyTime;
	}
}